﻿/* *
 * =============================================================================
 * File Name:               Device.cs
 * Programmer:              Ken Watts
 * Creation Date:           8/3/2008
 * =============================================================================
 * Description:             Class that represents the logic device superclass.
 * =============================================================================
 * Modification Date:       8/3/2008 - Initial Revision
 * =============================================================================
 * */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace DigitalLogicSimulator
{
    // Device class defines methods and properties common to all digital logice simulator devices
    public abstract class Device : Canvas
    {
        protected struct OutputDevice
        {
            public Device device;
            public int inputNumber;
            public int outputNumber;
            public Connection connection;
        }

        // Internal data
        protected double beginX, beginY;
        protected bool initiated = false, highlighted = false, moving=false, trackingMouseMove = false, selected = false;
        protected Point clickedPoint;
        DateTime dtLastClick=System.DateTime.Now;
        protected List<OutputDevice> outputDevices;

        // Constants
        public const double k_dbl_dash_size = 1.0;
        public const double k_dbl_stroke_thickness = 2.0;
        public const double k_dbl_stroke_highlight_thickness = 3.0;
        private const long k_long_max_ticks_between_clicks = 3000000;
        protected const int k_int_not_connected = -1;
        // Protected data
        protected Brush strokeBrush, focusBrush, backgroundBrush, highlightBrush;
        protected Color backgroundColor, highlightColor, strokeColor, rubberbandColor, focusColor;
        protected Line outputLine;
        protected Path path;
        protected PathGeometry pathGeometry;
        protected GeometryGroup pathGroup;
        protected Point outputLeft, outputRight;
        protected Device[] inputDevices;
        protected double canvLeft, canvTop, dblHighlightThickness = k_dbl_stroke_highlight_thickness;
        protected bool outputState = false, mouseOver=false;
        protected int currOutputNumber = 0;

        public Device()
        {
            Loaded += new RoutedEventHandler(Device_Loaded);
            // Setup the default colors
            backgroundColor = Color.FromArgb(0xaa, 0xff, 0xff, 0xff);
            highlightColor = Color.FromArgb(0xaa, 0xaa, 0xaa, 0xaa);
            strokeColor = Color.FromArgb(0xff, 0x54, 0x54, 0x54);
            rubberbandColor = Color.FromArgb(0xaa, 0, 0, 0);
            focusColor = Color.FromArgb(0xaa, 0x22, 0x8b, 0x22);
            // Create brushes
            strokeBrush = new SolidColorBrush(strokeColor);
            // Initialize this if "MouseEnter" is used
            focusBrush = null;
            backgroundBrush = new SolidColorBrush(backgroundColor);
            highlightBrush = new SolidColorBrush(highlightColor);

            // Setup the output device list
            outputDevices=new List<OutputDevice>();
            outputDevices.Clear();
        }

        protected abstract void Draw();

        // If successful, returns a positive integer representing the input number where the
        // connection is made. If not successful, k_int_not_connected (-1) is returned.
        protected abstract int Connect(Device logicDevice, Point clickedPoint);
        
        // Returns a point which represents the location of the specified input
        protected abstract Point GetInputLocation(int inputNumber);

        // Sets the state of the specified input to the given value
        public abstract void SetInputState(int inputNumber, bool state);

        // Returns the output location of the device
        public virtual Point GetOutputLocation()
        {
            return new Point(outputRight.X + canvLeft, outputRight.Y + canvTop + 1);
        }

        // Returns the output state
        protected bool GetOutputState()
        {
            return outputState;
        }

        // Updates the state of all connected output devices
        public virtual void UpdateOutputState()
        {
            foreach (OutputDevice od in outputDevices)
                od.device.SetInputState(od.inputNumber, outputState);
        }

        void DrawOutput()
        {
            foreach (OutputDevice od in outputDevices)
            {
                currOutputNumber = od.outputNumber;
                od.connection.Redraw( GetOutputLocation(),
                    od.device.GetInputLocation(od.inputNumber));
            }
        }

        void Device_Loaded(object sender, RoutedEventArgs e)
        {
            Draw();
        }

        public bool Contains(Point p)
        {
            // Determine if the given point is within the shape
            // kpw - 10/10/2008 - "HitTest" is no longer valid as of RC0 - Modified to compensate
            //            List<UIElement> hits = (List<UIElement>)HitTest(p);
            List<UIElement> hits = System.Windows.Media.VisualTreeHelper.
                FindElementsInHostCoordinates(p, this) as List<UIElement>;
            return hits.Contains(path);
        }

        public virtual void Highlight(bool on)
        {
            if (initiated)
            {
                if (path != null)
                {
                    if (on)
                    {
                        path.Fill = highlightBrush;
                    }
                    else
                    {
                        path.Fill = backgroundBrush;
                    }
                }
                highlighted = (highlightColor == backgroundColor) ? false : on;
            }
        }

        public bool IsHighlighted()
        {
            return highlighted;
        }

        public bool Disconnect(Connection c)
        {
            // Remove the given connection if it exists
            foreach (OutputDevice od in outputDevices)
                if (od.connection == c)
                {
                    // Reset the input on connected device
                    od.device.inputDevices[od.inputNumber] = null;
                    // Reset the input state
                    od.device.SetInputState(od.inputNumber, false);
                    outputDevices.Remove(od);
                    return true;
                }
            // Could not disconnect
            return false;
        }

        public bool Disconnect(Device d)
        {
            // Remove the given connection if it exists
            foreach (OutputDevice od in outputDevices)
                if (od.device == d)
                {
                    // Reset the input on connected device
                    od.device.inputDevices[od.inputNumber] = null;
                    // Reset the input state
                    od.device.SetInputState(od.inputNumber, false);
                    // Clear connection
                    od.connection.Delete();
                    outputDevices.Remove(od);
                    return true;
                }
            // Could not disconnect
            return false;
        }

        public bool Focused()
        {
            return (path.Stroke == focusBrush);
        }

        public void Delete()
        {
            // Disconnect all connected output devices
            foreach (OutputDevice od in outputDevices)
            {
                // Reset the input on connected device
                od.device.inputDevices[od.inputNumber] = null;
                // Reset the input state
                od.device.SetInputState(od.inputNumber, false);
                // Clear the connection
                od.connection.Delete();
            }
            // Clear the output device list
            outputDevices.Clear();
            // Disconnect all inputs
            if (inputDevices != null)
                foreach (Device dev in inputDevices)
                    if (dev != null)
                        dev.Disconnect(this);
            // Hide the shape
            path.Visibility = Visibility.Collapsed;
            selected = false;
        }

        public Point MakeConnection(Device dev, Point clickedPoint, Connection inConnection)
        {
            int inputNumber = dev.Connect(this, clickedPoint);
            if (inputNumber > k_int_not_connected) // Connection succeeded
            {
                // Set the input state on the connected device to this input state
                dev.SetInputState(inputNumber, (currOutputNumber==0?outputState:!outputState));
                OutputDevice od = new OutputDevice();
                od.device = dev;
                od.inputNumber = inputNumber;
                od.outputNumber = currOutputNumber;
                Connection c;
                if (inConnection != null)
                    c = inConnection;
                else
                {
                    c = new Connection(this,
                        GetOutputLocation(),
                        dev.GetInputLocation(inputNumber));
                    Panel parentPanel = (Panel)Parent;
                    parentPanel.Children.Add(c);
                }
                od.connection = c;
                outputDevices.Add(od);
            }
            return dev.GetInputLocation(inputNumber);
        }

        protected virtual void CustomMouseEnter(object sender, System.Windows.RoutedEventArgs e)
        {
            if ((focusBrush == null) && initiated)
                focusBrush = new SolidColorBrush(focusColor);
            if ((!trackingMouseMove) && initiated)
            {
                // Set the stroke color to the focus color
                path.Stroke = focusBrush;
            }
            if (!initiated)
                path.StrokeThickness = dblHighlightThickness;
            mouseOver = true;
        }

        protected virtual void CustomMouseLeave(
            object sender, System.Windows.RoutedEventArgs e)
        {
            // Reset the stroke color
            path.Stroke = strokeBrush;
            path.StrokeThickness = k_dbl_stroke_thickness;
            mouseOver = false;
        }

        protected virtual void ReplaceInstance(Panel parentPanel)
        {
        }

        protected void EnableDeleteButton(bool enabled)
        {
            // Find the delete button
            Panel parent = (Panel)Parent;
            Button buttonDelete;
            for (int i = 0; i < parent.Children.Count; i++)
                if (parent.Children[i] is Button)
                {
                    buttonDelete = (Button)parent.Children[i];
                    if (buttonDelete.Content.ToString() == "Delete")
                    {
                        buttonDelete.IsEnabled = enabled;
                        break;
                    }
                }
        }

        public void UpdateDeleteButtonState()
        {
            // Scan devices and enable delete button if any device is selected, disable otherwise
            Panel parent = (Panel)Parent;
            Device dev;
            bool foundSelectedDevice = false;
            for (int i = 0; i < parent.Children.Count; i++)
                if (parent.Children[i] is Device)
                {
                    dev = (Device)parent.Children[i];
                    if (dev.IsSelected())
                    {
                        foundSelectedDevice = true;
                        break;
                    }
                }
            EnableDeleteButton(foundSelectedDevice);
        }

        public bool IsSelected()
        {
            return selected;
        }

        protected virtual void CustomMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            beginX = e.GetPosition(this).X;
            beginY = e.GetPosition(this).Y;
            trackingMouseMove = true;
            moving = false;
            FrameworkElement fe = sender as FrameworkElement;
            canvLeft = Convert.ToDouble(fe.GetValue(Canvas.LeftProperty));
            canvTop = Convert.ToDouble(fe.GetValue(Canvas.TopProperty));
            // Reset the stroke color
            path.Stroke = strokeBrush;
            path.StrokeThickness = k_dbl_stroke_thickness;
            // Assume that this will be selected
            if(initiated)
                selected = !selected;

            if (fe != null)
            {
                fe.CaptureMouse();
            }

            // Reset z-orders
            Panel parentPanel = (Panel)Parent;
            int topMost = 0, zOrder;
            for (int i = 0; i < parentPanel.Children.Count; i++)
                if (parentPanel.Children[i] is Device)
                {
                    Device dev = (Device)parentPanel.Children[i];
                    // Determine the z-order of all item
                    zOrder = (int)dev.GetValue(Canvas.ZIndexProperty);
                    if (zOrder > topMost)
                        topMost = zOrder;
                }
            // Set this above the rest
            SetValue(Canvas.ZIndexProperty, topMost + 1);

            // Get the point that was clicked
            clickedPoint.X = beginX - canvLeft;
            clickedPoint.Y = beginY - canvTop;

            // Determine if output has been clicked
            if ((outputLine != null)&&initiated)
                if (clickedPoint.X > outputLeft.X)
                {
                    Point outputPoint = GetOutputLocation();
                    beginX = outputPoint.X;
                    beginY = outputPoint.Y;
                    outputLine.X1 = beginX;
                    outputLine.Y1 = beginY;
                    outputLine.X2 = beginX;
                    outputLine.Y2 = beginY;
                    outputLine.Visibility = System.Windows.Visibility.Visible;
                }
            // Note that the mouse event has been handled
            e.Handled = true;
        }

        protected void CustomMouseMove(object sender, MouseEventArgs e)
        {
            if (trackingMouseMove)
            {
                // Not selected
                selected = false;
                path.StrokeDashArray.Clear();
                if (!initiated)
                {
                    // Replace this instance
                    ReplaceInstance((Panel)Parent);
                    initiated = true;
                }
                double currentX = e.GetPosition(this).X;
                double currentY = e.GetPosition(this).Y;
                FrameworkElement fe = sender as FrameworkElement;
                canvLeft = Convert.ToDouble(fe.GetValue(Canvas.LeftProperty));
                canvTop = Convert.ToDouble(fe.GetValue(Canvas.TopProperty));
                path.Fill = backgroundBrush;

                // Check for rubber-band line
                bool rubberBandLineVisible = false;
                if (outputLine != null)
                    rubberBandLineVisible = (outputLine.Visibility == System.Windows.Visibility.Visible);
                if (rubberBandLineVisible)
                {
                    Panel parent = (Panel)Parent;
                    double xScale = 1, yScale = 1;
                    if (Application.Current.Host.Content.IsFullScreen)
                    {
                        xScale = Application.Current.Host.Content.ActualWidth / parent.ActualWidth;
                        yScale = Application.Current.Host.Content.ActualHeight / parent.ActualHeight;
                    }
                    if (currentX < parent.ActualWidth && currentX > 0 && currentY < parent.ActualHeight && currentY > 0)
                    {
                        outputLine.X1 = beginX;
                        outputLine.Y1 = beginY;
                        outputLine.X2 = currentX;
                        outputLine.Y2 = currentY;
                        for (int i = 0; i < parent.Children.Count; i++)
                            if (parent.Children[i] is Device)
                            {
                                Device dev = (Device)parent.Children[i];
                                // if the mouse is over another shape, highlight it
                                if (dev.Contains(new Point(e.GetPosition(parent).X * xScale, e.GetPosition(parent).Y * yScale)))
                                    dev.Highlight(true);
                                else
                                    dev.Highlight(false);
                            }
                    }
                }
                else // Move the shape
                {
                    double newLeft = canvLeft + currentX - beginX;
                    double newTop = canvTop + currentY - beginY;
                    FrameworkElement fe2 = (FrameworkElement)this.Parent;
                    double maxX, maxY;
                    moving = true;
                    if (pathGroup != null)
                    {
                        maxX = fe2.ActualWidth - pathGroup.Bounds.Width - 1;
                        maxY = fe2.ActualHeight - pathGroup.Bounds.Height - 1;
                    }
                    else
                    {
                        maxX = fe2.ActualWidth - pathGeometry.Bounds.Width - 1;
                        maxY = fe2.ActualHeight - pathGeometry.Bounds.Height - 1;
                    }

                    if (newLeft >= 0 && newLeft < maxX) beginX = currentX;
                    if (newTop >= 0 && newTop < maxY) beginY = currentY;

                    if (newLeft < 0) { newLeft = 0; beginX = clickedPoint.X; }
                    if (newTop < 0) { newTop = 0; beginY = clickedPoint.Y; }
                    if (newLeft > maxX) { newLeft = maxX; beginX = maxX + clickedPoint.X; }
                    if (newTop > maxY) { newTop = maxY; beginY = maxY + clickedPoint.Y; }

                    fe.SetValue(Canvas.LeftProperty, newLeft);
                    fe.SetValue(Canvas.TopProperty, newTop);

                    // Reset "canvLeft" and "canvTop" 
                    canvLeft = newLeft;
                    canvTop = newTop;

                    // Re-draw the output
                    DrawOutput();

                    // Have connected devices redraw the output as well
                    if (inputDevices != null)
                        foreach (Device dev in inputDevices)
                            if (dev != null)
                                dev.DrawOutput();
                }
            }
        }

        public void Unselect()
        {
            selected = false;
            path.StrokeDashArray.Clear();
            // Set the stroke color to the focus color
            if ((focusBrush != null) && mouseOver)
                path.Stroke = focusBrush;
            else
                path.Stroke = strokeBrush;
        }

        protected virtual void CustomMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            FrameworkElement fe = sender as FrameworkElement;
            fe.ReleaseMouseCapture();
            DateTime dtCurrClick = DateTime.Now;
            if (selected)
                if (dtCurrClick.Ticks - dtLastClick.Ticks > k_long_max_ticks_between_clicks)
                    selected = false;
            dtLastClick = dtCurrClick;
            moving = false;
            // If selected, show the "dashes"
            if (selected)
                path.StrokeDashArray.Add(k_dbl_dash_size);
            else
                path.StrokeDashArray.Clear();
            // Set the state of the delete button
            UpdateDeleteButtonState();
            // Set the stroke color to the focus color
            if ((focusBrush != null) && mouseOver)
                path.Stroke = focusBrush;
            else
                path.Stroke = strokeBrush;
            trackingMouseMove = false;
            Panel parent = (Panel)Parent;
            for (int i = 0; i < parent.Children.Count; i++)
                if (parent.Children[i] is Device)
                    if (outputLine != null)
                        if (outputLine.Visibility == System.Windows.Visibility.Visible)
                        {
                            Device dev = (Device)parent.Children[i];
                            double xScale = 1, yScale = 1;
                            if (Application.Current.Host.Content.IsFullScreen)
                            {
                                xScale = Application.Current.Host.Content.ActualWidth / parent.ActualWidth;
                                yScale = Application.Current.Host.Content.ActualHeight / parent.ActualHeight;
                            }
                            // if the mouse is over another shape, attempt connection
                            if (dev.IsHighlighted())
                            {
                                // Remove highlighting
                                dev.Highlight(false);
                                MakeConnection(dev, new Point(e.GetPosition(parent).X * xScale, e.GetPosition(parent).Y * yScale), null);
                            }
                        }
            if (outputLine != null)
                if (outputLine.Visibility == System.Windows.Visibility.Visible)
                {
                    outputLine.Visibility = System.Windows.Visibility.Collapsed;
                    path.Stroke = strokeBrush;
                }
            if (!initiated)
                CustomMouseEnter(sender, e);
        }
    }
}
